Modern JavaScript engines prevent GC from freezing the app through advanced techniques like incremental marking, concurrent sweeping, parallel threads, and idle-time collection, spreading GC work across time and CPU cores while the app continues running.
While it's true that JavaScript execution is single-threaded, the garbage collector is not limited to that same constraint. Modern engines like V8's Orinoco collector use a sophisticated combination of incremental, concurrent, and parallel techniques to perform garbage collection without stopping the world for long periods. The key insight is that the GC can do most of its work in background threads or in small, well-timed slices, allowing the main thread to continue executing JavaScript with minimal interruptions.
In early JavaScript engines, GC was fully stop-the-world—the entire program paused while the collector ran, causing noticeable UI jank in browsers and latency spikes in Node.js .
As web applications grew more complex, these pauses became unacceptable. A full GC cycle could take 100-300ms, causing dropped frames and poor user experience .
The challenge: the GC needs to traverse the entire object graph and cannot safely do so while JavaScript is modifying it, creating a fundamental tension .
The Orinoco project in V8 (and similar efforts in other engines) solved this through several key techniques that together eliminate long pauses.
Instead of marking all live objects in one long pause, the GC marks incrementally in small slices interleaved with JavaScript execution .
Each slice does a fixed amount of marking work (e.g., traversing a certain number of objects), then yields back to JavaScript .
This spreads a 100ms pause across 10ms slices over 10 task iterations—completely imperceptible to the user .
The challenge: JavaScript can modify object references between slices. Write barriers track these changes to ensure correctness .
Incremental marking reduced main thread pause times by 70% in V8 compared to stop-the-world marking .
Modern hardware has multiple cores. V8 uses helper threads to perform marking entirely in the background while JavaScript continues running on the main thread .
The main thread occasionally synchronizes with helper threads but doesn't wait for them. This is true concurrency, not parallelism .
Write barriers still needed: when JavaScript modifies an object, the barrier informs the concurrent marker about the change .
Concurrent marking eliminated most of the remaining marking pauses, especially for large heaps where incremental marking still caused noticeable delays .
Young generation collections (Scavenge) are fast but still need to pause the main thread briefly because they move objects .
Modern V8 uses parallel threads during this pause—multiple helper threads work simultaneously to evacuate live objects from the nursery .
With 4 helper threads, a 4ms pause can do the work that would have taken 16ms sequentially, remaining under the 16ms frame budget .
Parallel scavenging reduced young GC pause times by 20-50% in real-world workloads .
Browsers have idle periods between animation frames and user input. V8 uses the requestIdleCallback mechanism to perform GC work during these idle times .
The GC estimates how much time is available and does as much work as possible without exceeding the budget .
Idle-time collection can perform significant GC work that would otherwise need to happen during active execution .
This is particularly effective for background tabs or applications with idle periods, like text editors between keystrokes .
Because of the generational hypothesis, most collections are young generation collections, which are inherently fast (few milliseconds) .
Young GC happens frequently but pauses briefly; old GC happens rarely but uses concurrent techniques to avoid long pauses .
The combination means that in a typical web app, 95% of GC pauses are under 5ms—well within the 16ms frame budget for 60fps .
Large, long-running apps (like Figma or Google Docs) still trigger full collections, but these are now concurrent and invisible .
Large object allocation: Allocating a huge array (e.g., 100MB) can still cause a pause because memory must be zeroed .
GC stress: Applications that allocate extremely fast may force GC to work harder, potentially causing noticeable pauses despite optimizations .
Real-time requirements: For audio processing or VR, even 5ms pauses can be problematic. These use cases may need WebAssembly or special GC avoidance techniques .
Mobile devices: Lower-powered devices have fewer CPU cores and slower memory, making concurrent GC less effective and pauses more noticeable .
The evolution of garbage collection from stop-the-world to incremental, concurrent, and parallel represents one of the most significant improvements in JavaScript engine performance. It's why modern web applications can run for hours without jank, why Node.js servers can handle thousands of requests with consistent latency, and why we rarely think about GC pauses despite JavaScript being single-threaded. The GC works in the shadows, using idle time and spare CPU cores to keep the main thread free for what matters: running your code.